"""
Input validation utilities for the data preprocessing pipeline.

This module provides comprehensive validation functions for user inputs,
file operations, and data structures with integrated error handling.
"""

import os
import pandas as pd
from pathlib import Path
from typing import List, Optional, Any, Union, Tuple

from .exceptions import (
    DataValidationError, FileOperationError, ConfigurationError,
    PreprocessorError, validate_input_parameter
)


class InputValidator:
    """
    Comprehensive input validation for the preprocessing pipeline.
    
    This class provides methods to validate various types of inputs
    including files, data structures, configuration parameters, and user inputs.
    """
    
    def __init__(self):
        """Initialize the input validator."""
        self.supported_excel_extensions = {'.xlsx', '.xls'}
        self.min_layers = 1
        self.max_layers = 10
    
    # File Validation Methods
    
    def validate_file_path(self, file_path: str, must_exist: bool = True) -> str:
        """
        Validate a file path.
        
        Args:
            file_path (str): Path to validate
            must_exist (bool): Whether the file must exist
            
        Returns:
            str: Validated file path
            
        Raises:
            FileNotFoundError: If file doesn't exist and must_exist is True
            FileAccessError: If file exists but cannot be accessed
        """
        if not file_path or not isinstance(file_path, str):
            raise UserInputError(
                input_type="file_path",
                input_value=file_path,
                expected_format="non-empty string",
                message="File path must be a non-empty string"
            )
        
        path_obj = Path(file_path)
        
        if must_exist:
            if not path_obj.exists():
                raise FileOperationError(
                    "File does not exist",
                    file_path=file_path,
                    operation="validate"
                )
            
            if not path_obj.is_file():
                raise FileOperationError(
                    "Path is not a file",
                    file_path=file_path,
                    operation="validate",
                    details="Path points to a directory or special file"
                )
            
            if not os.access(file_path, os.R_OK):
                raise FileOperationError(
                    "File is not readable",
                    file_path=file_path,
                    operation="validate",
                    details="Check file permissions"
                )
        
        return str(path_obj.resolve())
    
    def validate_excel_file(self, file_path: str) -> str:
        """
        Validate an Excel file.
        
        Args:
            file_path (str): Path to the Excel file
            
        Returns:
            str: Validated file path
            
        Raises:
            FileFormatError: If file format is not supported
            FileNotFoundError: If file doesn't exist
            FileAccessError: If file cannot be accessed
        """
        # First validate the basic file path
        validated_path = self.validate_file_path(file_path, must_exist=True)
        
        # Check file extension
        path_obj = Path(validated_path)
        extension = path_obj.suffix.lower()
        
        if extension not in self.supported_excel_extensions:
            raise FileOperationError(
                f"Unsupported file format '{extension}'",
                file_path=validated_path,
                operation="validate",
                details=f"Supported formats: {', '.join(self.supported_excel_extensions)}"
            )
        
        # Check file size (should not be empty)
        if path_obj.stat().st_size == 0:
            raise DataValidationError(
                validation_type="file_size",
                message=f"Excel file is empty: {validated_path}",
                data_info={'file_path': validated_path, 'size_bytes': 0}
            )
        
        return validated_path
    
    def validate_output_directory(self, directory_path: str, create_if_missing: bool = True) -> str:
        """
        Validate an output directory path.
        
        Args:
            directory_path (str): Path to the output directory
            create_if_missing (bool): Whether to create the directory if it doesn't exist
            
        Returns:
            str: Validated directory path
            
        Raises:
            FileAccessError: If directory cannot be created or accessed
        """
        if not directory_path or not isinstance(directory_path, str):
            raise UserInputError(
                input_type="directory_path",
                input_value=directory_path,
                expected_format="non-empty string",
                message="Directory path must be a non-empty string"
            )
        
        path_obj = Path(directory_path)
        
        if path_obj.exists():
            if not path_obj.is_dir():
                raise FileAccessError(
                    directory_path,
                    "write",
                    f"Path exists but is not a directory: {directory_path}"
                )
            
            if not os.access(directory_path, os.W_OK):
                raise FileAccessError(
                    directory_path,
                    "write",
                    f"Directory exists but is not writable: {directory_path}"
                )
        elif create_if_missing:
            try:
                path_obj.mkdir(parents=True, exist_ok=True)
            except OSError as e:
                raise FileAccessError(
                    directory_path,
                    "create",
                    f"Cannot create directory: {str(e)}"
                ) from e
        else:
            raise FileNotFoundError(directory_path)
        
        return str(path_obj.resolve())
    
    # Data Validation Methods
    
    def validate_dataframe(self, data: Any, allow_empty: bool = False) -> pd.DataFrame:
        """
        Validate a pandas DataFrame.
        
        Args:
            data (Any): Data to validate
            allow_empty (bool): Whether to allow empty DataFrames
            
        Returns:
            pd.DataFrame: Validated DataFrame
            
        Raises:
            DataValidationError: If data is invalid
        """
        if data is None:
            raise DataValidationError(
                validation_type="null_check",
                message="Data cannot be None"
            )
        
        if not isinstance(data, pd.DataFrame):
            raise DataValidationError(
                validation_type="type_check",
                message=f"Data must be a pandas DataFrame, got {type(data).__name__}",
                data_info={'actual_type': type(data).__name__}
            )
        
        if not allow_empty and data.empty:
            raise DataValidationError(
                validation_type="empty_check",
                message="DataFrame cannot be empty",
                data_info={'shape': data.shape}
            )
        
        return data
    
    def validate_excel_data_structure(self, data: pd.DataFrame) -> pd.DataFrame:
        """
        Validate Excel data structure for preprocessing.
        
        Args:
            data (pd.DataFrame): DataFrame to validate
            
        Returns:
            pd.DataFrame: Validated DataFrame
            
        Raises:
            DataStructureError: If data structure is invalid
        """
        # First validate it's a proper DataFrame
        data = self.validate_dataframe(data, allow_empty=False)
        
        if len(data.columns) < 2:
            raise DataStructureError(
                expected_structure="at least 2 columns (ID column + data columns)",
                actual_structure=f"{len(data.columns)} columns",
                message="Excel data must have at least 2 columns: one ID column and at least one data column"
            )
        
        # Check that data columns (all except first) are numeric
        data_columns = data.columns[1:]  # Skip first column (ID column)
        non_numeric_columns = []
        
        for col in data_columns:
            if not pd.api.types.is_numeric_dtype(data[col]):
                # Try to convert to numeric
                try:
                    pd.to_numeric(data[col], errors='raise')
                except (ValueError, TypeError):
                    non_numeric_columns.append(col)
        
        if non_numeric_columns:
            raise DataStructureError(
                expected_structure="numeric data columns (except first column)",
                actual_structure=f"non-numeric columns: {non_numeric_columns}",
                message=f"Data columns must be numeric. Non-numeric columns found: {non_numeric_columns}"
            )
        
        return data
    
    # Configuration Validation Methods
    
    def validate_layer_count(self, layer_count: Any) -> int:
        """
        Validate layer count input.
        
        Args:
            layer_count (Any): Layer count to validate
            
        Returns:
            int: Validated layer count
            
        Raises:
            InvalidLayerCountError: If layer count is invalid
        """
        # Check if it's a valid integer
        try:
            if isinstance(layer_count, str):
                layer_count = int(layer_count.strip())
            elif not isinstance(layer_count, int):
                layer_count = int(layer_count)
        except (ValueError, TypeError):
            raise UserInputError(
                input_type="layer_count",
                input_value=layer_count,
                expected_format="integer",
                message="Layer count must be a valid integer"
            )
        
        # Check range
        if layer_count < self.min_layers or layer_count > self.max_layers:
            raise InvalidLayerCountError(layer_count, self.min_layers, self.max_layers)
        
        return layer_count
    
    def validate_method_selections(self, layer_selections: List[List[int]], 
                                 available_methods: List[int]) -> List[List[int]]:
        """
        Validate method selections for all layers.
        
        Args:
            layer_selections (List[List[int]]): Method selections for each layer
            available_methods (List[int]): Available method IDs
            
        Returns:
            List[List[int]]: Validated layer selections
            
        Raises:
            EmptyLayerSelectionError: If any layer has no methods selected
            InvalidMethodSelectionError: If invalid methods are selected
        """
        if not layer_selections:
            raise UserInputError(
                input_type="layer_selections",
                input_value="empty list",
                expected_format="non-empty list of method selections",
                message="Layer selections cannot be empty"
            )
        
        validated_selections = []
        
        for layer_index, layer_methods in enumerate(layer_selections):
            # Check if layer has any methods
            if not layer_methods:
                raise EmptyLayerSelectionError(layer_index)
            
            # Validate each method ID
            invalid_methods = []
            for method_id in layer_methods:
                if not isinstance(method_id, int):
                    try:
                        method_id = int(method_id)
                    except (ValueError, TypeError):
                        invalid_methods.append(method_id)
                        continue
                
                if method_id not in available_methods:
                    invalid_methods.append(method_id)
            
            if invalid_methods:
                raise InvalidMethodSelectionError(
                    layer_index, invalid_methods, available_methods
                )
            
            # Remove duplicates and sort
            validated_layer_methods = sorted(list(set(layer_methods)))
            validated_selections.append(validated_layer_methods)
        
        return validated_selections
    
    def validate_method_id_input(self, method_input: str, available_methods: List[int]) -> List[int]:
        """
        Validate and parse method ID input string.
        
        Args:
            method_input (str): Comma-separated method IDs
            available_methods (List[int]): Available method IDs
            
        Returns:
            List[int]: Parsed and validated method IDs
            
        Raises:
            UserInputError: If input format is invalid
            InvalidMethodSelectionError: If invalid methods are specified
        """
        if not isinstance(method_input, str):
            raise UserInputError(
                input_type="method_input",
                input_value=method_input,
                expected_format="comma-separated integers",
                message="Method input must be a string"
            )
        
        method_input = method_input.strip()
        
        # Handle empty input (default to method 0)
        if not method_input:
            return [0] if 0 in available_methods else []
        
        # Parse comma-separated values
        method_ids = []
        invalid_parts = []
        
        for part in method_input.split(','):
            part = part.strip()
            if not part:
                continue
            
            try:
                method_id = int(part)
                if method_id not in method_ids:  # Avoid duplicates
                    method_ids.append(method_id)
            except ValueError:
                invalid_parts.append(part)
        
        if invalid_parts:
            raise UserInputError(
                input_type="method_input",
                input_value=method_input,
                expected_format="comma-separated integers",
                message=f"Invalid method IDs: {invalid_parts}"
            )
        
        if not method_ids:
            raise UserInputError(
                input_type="method_input",
                input_value=method_input,
                expected_format="at least one valid method ID",
                message="At least one valid method ID must be specified"
            )
        
        # Check if all methods are available
        invalid_methods = [mid for mid in method_ids if mid not in available_methods]
        if invalid_methods:
            raise InvalidMethodSelectionError(0, invalid_methods, available_methods)
        
        return sorted(method_ids)
    
    # Utility Validation Methods
    
    def validate_positive_integer(self, value: Any, name: str, 
                                min_value: int = 1, max_value: Optional[int] = None) -> int:
        """
        Validate a positive integer input.
        
        Args:
            value (Any): Value to validate
            name (str): Name of the parameter for error messages
            min_value (int): Minimum allowed value
            max_value (Optional[int]): Maximum allowed value
            
        Returns:
            int: Validated integer
            
        Raises:
            UserInputError: If value is invalid
        """
        try:
            if isinstance(value, str):
                value = int(value.strip())
            elif not isinstance(value, int):
                value = int(value)
        except (ValueError, TypeError):
            raise UserInputError(
                input_type=name,
                input_value=value,
                expected_format="positive integer",
                message=f"{name} must be a valid integer"
            )
        
        if value < min_value:
            raise UserInputError(
                input_type=name,
                input_value=value,
                expected_format=f"integer >= {min_value}",
                message=f"{name} must be at least {min_value}"
            )
        
        if max_value is not None and value > max_value:
            raise UserInputError(
                input_type=name,
                input_value=value,
                expected_format=f"integer <= {max_value}",
                message=f"{name} must be at most {max_value}"
            )
        
        return value
    
    def validate_boolean_input(self, value: Any, name: str) -> bool:
        """
        Validate a boolean input (including string representations).
        
        Args:
            value (Any): Value to validate
            name (str): Name of the parameter for error messages
            
        Returns:
            bool: Validated boolean
            
        Raises:
            UserInputError: If value is invalid
        """
        if isinstance(value, bool):
            return value
        
        if isinstance(value, str):
            value_lower = value.strip().lower()
            if value_lower in ['true', 'yes', 'y', '1', 'on']:
                return True
            elif value_lower in ['false', 'no', 'n', '0', 'off']:
                return False
        
        raise UserInputError(
            input_type=name,
            input_value=value,
            expected_format="boolean (true/false, yes/no, y/n, 1/0)",
            message=f"{name} must be a valid boolean value"
        )
    
    def validate_choice_input(self, value: Any, choices: List[Any], name: str) -> Any:
        """
        Validate that input is one of the allowed choices.
        
        Args:
            value (Any): Value to validate
            choices (List[Any]): List of allowed choices
            name (str): Name of the parameter for error messages
            
        Returns:
            Any: Validated choice
            
        Raises:
            UserInputError: If value is not in choices
        """
        if value not in choices:
            raise UserInputError(
                input_type=name,
                input_value=value,
                expected_format=f"one of {choices}",
                message=f"{name} must be one of: {choices}"
            )
        
        return value


# Global validator instance
_global_validator = None


def get_validator() -> InputValidator:
    """
    Get the global input validator instance.
    
    Returns:
        InputValidator: The global validator
    """
    global _global_validator
    if _global_validator is None:
        _global_validator = InputValidator()
    return _global_validator


# Convenience functions for common validations

def validate_excel_file(file_path: str) -> str:
    """Validate an Excel file path."""
    return get_validator().validate_excel_file(file_path)






    def validate_file_path(file_path: str, must_exist: bool = True, 
                          must_be_readable: bool = True) -> None:
        """
        Validate file path and accessibility.
        
        Args:
            file_path (str): Path to validate
            must_exist (bool): Whether file must exist
            must_be_readable (bool): Whether file must be readable
            
        Raises:
            FileOperationError: If validation fails
        """
        if not file_path or not isinstance(file_path, str):
            raise FileOperationError(
                "Invalid file path",
                file_path=str(file_path),
                operation="validate",
                details="File path must be a non-empty string"
            )
        
        file_path = file_path.strip()
        if not file_path:
            raise FileOperationError(
                "Empty file path",
                file_path=file_path,
                operation="validate"
            )
        
        if must_exist:
            if not os.path.exists(file_path):
                raise FileOperationError(
                    "File does not exist",
                    file_path=file_path,
                    operation="validate"
                )
            
            if not os.path.isfile(file_path):
                raise FileOperationError(
                    "Path is not a file",
                    file_path=file_path,
                    operation="validate",
                    details="Path points to a directory or special file"
                )
        
        if must_be_readable and must_exist:
            if not os.access(file_path, os.R_OK):
                raise FileOperationError(
                    "File is not readable",
                    file_path=file_path,
                    operation="validate",
                    details="Check file permissions"
                )
    
    @staticmethod
    def validate_excel_file(file_path: str) -> None:
        """
        Validate Excel file format and basic properties.
        
        Args:
            file_path (str): Path to Excel file
            
        Raises:
            FileOperationError: If validation fails
        """
        # Basic file validation
        InputValidator.validate_file_path(file_path, must_exist=True, must_be_readable=True)
        
        # Check file extension
        file_ext = Path(file_path).suffix.lower()
        if file_ext not in InputValidator.SUPPORTED_EXCEL_EXTENSIONS:
            raise FileOperationError(
                f"Unsupported file format '{file_ext}'",
                file_path=file_path,
                operation="validate",
                details=f"Supported formats: {', '.join(InputValidator.SUPPORTED_EXCEL_EXTENSIONS)}"
            )
        
        # Check file size (not empty)
        try:
            file_size = os.path.getsize(file_path)
            if file_size == 0:
                raise FileOperationError(
                    "File is empty",
                    file_path=file_path,
                    operation="validate"
                )
        except OSError as e:
            raise FileOperationError(
                "Cannot access file size",
                file_path=file_path,
                operation="validate",
                details=str(e)
            )
    
    @staticmethod
    def validate_dataframe_structure(data: pd.DataFrame, min_columns: int = 2,
                                   min_rows: int = 1) -> None:
        """
        Validate DataFrame structure for preprocessing pipeline.
        
        Args:
            data (pd.DataFrame): DataFrame to validate
            min_columns (int): Minimum required columns
            min_rows (int): Minimum required rows
            
        Raises:
            DataValidationError: If validation fails
        """
        if data is None:
            raise DataValidationError(
                "DataFrame is None",
                validation_type="structure",
                expected_format="pandas DataFrame",
                actual_format="None"
            )
        
        if not isinstance(data, pd.DataFrame):
            raise DataValidationError(
                "Invalid data type",
                validation_type="structure",
                expected_format="pandas DataFrame",
                actual_format=type(data).__name__
            )
        
        if data.empty:
            raise DataValidationError(
                "DataFrame is empty",
                validation_type="structure",
                expected_format=f"At least {min_rows} rows and {min_columns} columns",
                actual_format="Empty DataFrame"
            )
        
        if len(data.columns) < min_columns:
            raise DataValidationError(
                f"Insufficient columns",
                validation_type="structure",
                expected_format=f"At least {min_columns} columns",
                actual_format=f"{len(data.columns)} columns"
            )
        
        if len(data) < min_rows:
            raise DataValidationError(
                f"Insufficient rows",
                validation_type="structure",
                expected_format=f"At least {min_rows} rows",
                actual_format=f"{len(data)} rows"
            )
    
    @staticmethod
    def validate_preprocessing_data_structure(data: pd.DataFrame) -> Tuple[str, List[str]]:
        """
        Validate DataFrame structure for preprocessing (ID column + numeric columns).
        
        Args:
            data (pd.DataFrame): DataFrame to validate
            
        Returns:
            Tuple[str, List[str]]: (id_column_name, numeric_column_names)
            
        Raises:
            DataValidationError: If validation fails
        """
        # Basic structure validation
        InputValidator.validate_dataframe_structure(data, min_columns=2, min_rows=1)
        
        columns = data.columns.tolist()
        id_column = columns[0]
        numeric_columns = columns[1:]
        
        # Validate numeric columns
        invalid_columns = []
        for col in numeric_columns:
            if not InputValidator._is_numeric_column(data[col]):
                invalid_columns.append(col)
        
        if invalid_columns:
            raise DataValidationError(
                f"Non-numeric columns found: {', '.join(invalid_columns)}",
                validation_type="data_types",
                expected_format="Numeric data in all columns except the first",
                actual_format=f"Non-numeric data in columns: {invalid_columns}"
            )
        
        # Check for completely empty columns
        empty_columns = []
        for col in numeric_columns:
            if data[col].isna().all():
                empty_columns.append(col)
        
        if empty_columns:
            raise DataValidationError(
                f"Empty columns found: {', '.join(empty_columns)}",
                validation_type="data_completeness",
                expected_format="At least some data in each column",
                actual_format=f"Completely empty columns: {empty_columns}"
            )
        
        return id_column, numeric_columns
    
    @staticmethod
    def _is_numeric_column(series: pd.Series, max_non_numeric_ratio: float = 0.1) -> bool:
        """
        Check if a pandas Series can be considered numeric.
        
        Args:
            series (pd.Series): Series to check
            max_non_numeric_ratio (float): Maximum ratio of non-numeric values allowed
            
        Returns:
            bool: True if series is numeric or can be converted to numeric
        """
        # Already numeric
        if pd.api.types.is_numeric_dtype(series):
            return True
        
        # Try to convert to numeric
        try:
            numeric_series = pd.to_numeric(series, errors='coerce')
            non_numeric_count = numeric_series.isna().sum() - series.isna().sum()
            non_numeric_ratio = non_numeric_count / len(series)
            return non_numeric_ratio <= max_non_numeric_ratio
        except:
            return False
    
    @staticmethod
    def validate_layer_count(layer_count: Any) -> int:
        """
        Validate layer count input.
        
        Args:
            layer_count: Input to validate
            
        Returns:
            int: Validated layer count
            
        Raises:
            ConfigurationError: If validation fails
        """
        validate_input_parameter(
            layer_count, "layer_count", int,
            min_value=InputValidator.MIN_LAYER_COUNT,
            max_value=InputValidator.MAX_LAYER_COUNT
        )
        return layer_count
    
    @staticmethod
    def validate_method_selections(layer_selections: List[List[int]], 
                                 available_methods: List[int]) -> None:
        """
        Validate method selections for all layers.
        
        Args:
            layer_selections (List[List[int]]): Selected methods for each layer
            available_methods (List[int]): Available method IDs
            
        Raises:
            ConfigurationError: If validation fails
        """
        if not isinstance(layer_selections, list):
            raise ConfigurationError(
                "Layer selections must be a list",
                parameter="layer_selections",
                provided_value=type(layer_selections).__name__
            )
        
        if not layer_selections:
            raise ConfigurationError(
                "Layer selections cannot be empty",
                parameter="layer_selections"
            )
        
        for layer_index, layer_methods in enumerate(layer_selections):
            if not isinstance(layer_methods, list):
                raise ConfigurationError(
                    f"Layer {layer_index + 1} selections must be a list",
                    parameter=f"layer_{layer_index + 1}_selections",
                    provided_value=type(layer_methods).__name__
                )
            
            if not layer_methods:
                raise ConfigurationError(
                    f"Layer {layer_index + 1} must have at least one method selected",
                    parameter=f"layer_{layer_index + 1}_selections"
                )
            
            for method_id in layer_methods:
                if not isinstance(method_id, int):
                    raise ConfigurationError(
                        f"Method ID must be an integer in layer {layer_index + 1}",
                        parameter=f"layer_{layer_index + 1}_method_id",
                        provided_value=f"{type(method_id).__name__}: {method_id}"
                    )
                
                if method_id not in available_methods:
                    raise ConfigurationError(
                        f"Invalid method ID {method_id} in layer {layer_index + 1}",
                        parameter=f"layer_{layer_index + 1}_method_id",
                        valid_range=f"Available methods: {available_methods}",
                        provided_value=method_id
                    )
    
    @staticmethod
    def validate_combination(combination: List[int], available_methods: List[int]) -> None:
        """
        Validate a single preprocessing combination.
        
        Args:
            combination (List[int]): Combination to validate
            available_methods (List[int]): Available method IDs
            
        Raises:
            PreprocessorError: If validation fails
        """
        if not isinstance(combination, list):
            raise PreprocessorError(
                "Combination must be a list",
                details=f"Got {type(combination).__name__}: {combination}"
            )
        
        if not combination:
            raise PreprocessorError("Combination cannot be empty")
        
        for i, method_id in enumerate(combination):
            if not isinstance(method_id, int):
                raise PreprocessorError(
                    f"Method ID at position {i} must be an integer",
                    processor_id=method_id,
                    layer_index=i,
                    details=f"Got {type(method_id).__name__}: {method_id}"
                )
            
            if method_id not in available_methods:
                raise PreprocessorError(
                    f"Method ID {method_id} not found in registry",
                    processor_id=method_id,
                    layer_index=i,
                    details=f"Available methods: {available_methods}"
                )
    
    @staticmethod
    def validate_output_directory(output_dir: str, create_if_missing: bool = True) -> str:
        """
        Validate and optionally create output directory.
        
        Args:
            output_dir (str): Output directory path
            create_if_missing (bool): Whether to create directory if it doesn't exist
            
        Returns:
            str: Validated output directory path
            
        Raises:
            FileOperationError: If validation fails
        """
        if not output_dir or not isinstance(output_dir, str):
            raise FileOperationError(
                "Invalid output directory path",
                file_path=str(output_dir),
                operation="validate",
                details="Output directory must be a non-empty string"
            )
        
        output_dir = output_dir.strip()
        if not output_dir:
            raise FileOperationError(
                "Empty output directory path",
                file_path=output_dir,
                operation="validate"
            )
        
        # Convert to absolute path
        output_dir = os.path.abspath(output_dir)
        
        if os.path.exists(output_dir):
            if not os.path.isdir(output_dir):
                raise FileOperationError(
                    "Output path exists but is not a directory",
                    file_path=output_dir,
                    operation="validate"
                )
            
            if not os.access(output_dir, os.W_OK):
                raise FileOperationError(
                    "Output directory is not writable",
                    file_path=output_dir,
                    operation="validate",
                    details="Check directory permissions"
                )
        elif create_if_missing:
            try:
                os.makedirs(output_dir, exist_ok=True)
            except OSError as e:
                raise FileOperationError(
                    "Cannot create output directory",
                    file_path=output_dir,
                    operation="create",
                    details=str(e)
                )
        
        return output_dir
    
    @staticmethod
    def validate_filename(filename: str, max_length: int = 255) -> str:
        """
        Validate and sanitize filename.
        
        Args:
            filename (str): Filename to validate
            max_length (int): Maximum filename length
            
        Returns:
            str: Validated and sanitized filename
            
        Raises:
            FileOperationError: If validation fails
        """
        if not filename or not isinstance(filename, str):
            raise FileOperationError(
                "Invalid filename",
                file_path=str(filename),
                operation="validate",
                details="Filename must be a non-empty string"
            )
        
        filename = filename.strip()
        if not filename:
            raise FileOperationError(
                "Empty filename",
                file_path=filename,
                operation="validate"
            )
        
        # Check length
        if len(filename) > max_length:
            raise FileOperationError(
                f"Filename too long ({len(filename)} characters)",
                file_path=filename,
                operation="validate",
                details=f"Maximum length: {max_length} characters"
            )
        
        # Check for invalid characters (Windows + Unix)
        invalid_chars = r'[<>:"/\\|?*\x00-\x1f]'
        if re.search(invalid_chars, filename):
            raise FileOperationError(
                "Filename contains invalid characters",
                file_path=filename,
                operation="validate",
                details="Invalid characters: < > : \" / \\ | ? * and control characters"
            )
        
        # Check for reserved names (Windows)
        reserved_names = {
            'CON', 'PRN', 'AUX', 'NUL',
            'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
            'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'
        }
        
        name_without_ext = os.path.splitext(filename)[0].upper()
        if name_without_ext in reserved_names:
            raise FileOperationError(
                f"Filename uses reserved name '{name_without_ext}'",
                file_path=filename,
                operation="validate",
                details=f"Reserved names: {', '.join(reserved_names)}"
            )
        
        return filename
    
    @staticmethod
    def check_combination_count_warning(layer_selections: List[List[int]]) -> Tuple[int, bool]:
        """
        Calculate combination count and check if warning threshold is exceeded.
        
        Args:
            layer_selections (List[List[int]]): Selected methods for each layer
            
        Returns:
            Tuple[int, bool]: (total_combinations, should_warn)
        """
        if not layer_selections:
            return 0, False
        
        total_combinations = 1
        for layer_methods in layer_selections:
            if layer_methods:
                total_combinations *= len(layer_methods)
        
        should_warn = total_combinations > InputValidator.COMBINATION_WARNING_THRESHOLD
        return total_combinations, should_warn
    
    @staticmethod
    def validate_user_input_string(user_input: str, parameter_name: str,
                                 allow_empty: bool = False, max_length: int = 1000) -> str:
        """
        Validate user input string.
        
        Args:
            user_input (str): User input to validate
            parameter_name (str): Parameter name for error reporting
            allow_empty (bool): Whether empty strings are allowed
            max_length (int): Maximum string length
            
        Returns:
            str: Validated and stripped input string
            
        Raises:
            ConfigurationError: If validation fails
        """
        if user_input is None:
            if allow_empty:
                return ""
            raise ConfigurationError(
                f"Parameter '{parameter_name}' cannot be None",
                parameter=parameter_name
            )
        
        if not isinstance(user_input, str):
            raise ConfigurationError(
                f"Parameter '{parameter_name}' must be a string",
                parameter=parameter_name,
                provided_value=f"{type(user_input).__name__}: {user_input}"
            )
        
        user_input = user_input.strip()
        
        if not allow_empty and not user_input:
            raise ConfigurationError(
                f"Parameter '{parameter_name}' cannot be empty",
                parameter=parameter_name
            )
        
        if len(user_input) > max_length:
            raise ConfigurationError(
                f"Parameter '{parameter_name}' is too long ({len(user_input)} characters)",
                parameter=parameter_name,
                valid_range=f"Maximum length: {max_length} characters",
                provided_value=f"Length: {len(user_input)}"
            )
        
        return user_input


class DataQualityChecker:
    """
    Advanced data quality checking for preprocessing pipeline.
    
    This class provides methods to analyze data quality issues that might
    affect preprocessing results.
    """
    
    @staticmethod
    def analyze_data_quality(data: pd.DataFrame) -> dict:
        """
        Perform comprehensive data quality analysis.
        
        Args:
            data (pd.DataFrame): DataFrame to analyze
            
        Returns:
            dict: Data quality analysis results
        """
        if data is None or data.empty:
            return {"error": "No data to analyze"}
        
        analysis = {
            "shape": data.shape,
            "columns": list(data.columns),
            "dtypes": data.dtypes.to_dict(),
            "missing_values": {},
            "duplicate_rows": 0,
            "numeric_columns": [],
            "non_numeric_columns": [],
            "warnings": [],
            "recommendations": []
        }
        
        # Missing values analysis
        missing_counts = data.isnull().sum()
        for col, count in missing_counts.items():
            if count > 0:
                percentage = (count / len(data)) * 100
                analysis["missing_values"][col] = {
                    "count": int(count),
                    "percentage": round(percentage, 2)
                }
                
                if percentage > 50:
                    analysis["warnings"].append(
                        f"Column '{col}' has {percentage:.1f}% missing values"
                    )
        
        # Duplicate rows
        analysis["duplicate_rows"] = int(data.duplicated().sum())
        if analysis["duplicate_rows"] > 0:
            analysis["warnings"].append(
                f"Found {analysis['duplicate_rows']} duplicate rows"
            )
        
        # Column type analysis
        for col in data.columns:
            if InputValidator._is_numeric_column(data[col]):
                analysis["numeric_columns"].append(col)
            else:
                analysis["non_numeric_columns"].append(col)
        
        # Generate recommendations
        if analysis["missing_values"]:
            analysis["recommendations"].append(
                "Consider handling missing values before preprocessing"
            )
        
        if analysis["duplicate_rows"] > 0:
            analysis["recommendations"].append(
                "Consider removing duplicate rows"
            )
        
        if len(analysis["non_numeric_columns"]) > 1:  # More than just ID column
            analysis["recommendations"].append(
                "Non-numeric columns detected - ensure only ID column is non-numeric"
            )
        
        return analysis
    
    @staticmethod
    def check_data_consistency(data: pd.DataFrame) -> List[str]:
        """
        Check for data consistency issues.
        
        Args:
            data (pd.DataFrame): DataFrame to check
            
        Returns:
            List[str]: List of consistency issues found
        """
        issues = []
        
        if data is None or data.empty:
            return ["No data to check"]
        
        # Check for infinite values
        numeric_cols = data.select_dtypes(include=[np.number]).columns
        for col in numeric_cols:
            if np.isinf(data[col]).any():
                issues.append(f"Column '{col}' contains infinite values")
        
        # Check for extremely large values that might cause issues
        for col in numeric_cols:
            max_val = data[col].max()
            min_val = data[col].min()
            if pd.notna(max_val) and abs(max_val) > 1e10:
                issues.append(f"Column '{col}' has very large values (max: {max_val})")
            if pd.notna(min_val) and abs(min_val) > 1e10:
                issues.append(f"Column '{col}' has very large values (min: {min_val})")
        
        # Check for columns with zero variance
        for col in numeric_cols:
            if data[col].nunique() == 1:
                issues.append(f"Column '{col}' has zero variance (all values are the same)")
        
        return issues